const isString = val => typeof val === 'string';
//用来判断v不是未定义的，也就是说是null或者是undefined
function isUndef(v) {
    return v === undefined || v === null;
}
//用来判断v是定义过的，也就是不是null也不是undefined
function isDef(v) {
    return v !== undefined && v !== null;
}
//判断2个虚拟DOM节点是不是同一个节点
function sameVnode(a, b) {
    //如果两个虚拟DOM节点key是一样并且tag也就是类型是一样的
    return a.key === b.key && a.tag === b.tag;//tag type都指元素的类型span div
}

function vnode(tag, data, key, children, text) {
    return { tag, data, key, children, text };
}
/**
 * 创建虚拟DOM节点
 * @param {*} tag  节点的类型
 * @param {*} data 节点的属性
 * @param {*} children 子节点，可能没有，可能有一个，可能有多个，反正都是数组
*/
export function createElement(tag, data = {}, ...children) {
    let key = data.key;//获取属性对象中的key属性
    if (key) {
        delete data.key;//删除属性对象中的key属性
    }
    return vnode(tag, data, key, children);
}
export function createTextNode(text) {
    return vnode(undefined, undefined, undefined, undefined, text);
}
/**
 * 更新真实DOM的属性
 * @param {*} vnode 虚拟DOM
 * @param {*} oldProps 老属性对象
 * @param {*} newProps 新属性对象
 */
function updateDOMProperties(elm, oldProps = {}, newProps = {}) {
    //如果老的属性在新的属性对象里不存在，则意味着此属性要删除掉
    for (let key in oldProps) {
        if (!newProps.hasOwnProperty(key)) {
            elm.setAttribute(key, null);
        }
    }
    //遍历新的属性对象
    for (let key in newProps) {
        //如果是属性名是style的话
        if (key === 'style') {
            //遍历样式对象
            for (let styleName in newProps.style) {
                //把样式对象中的属性和值赋值给真实DOM
                elm.style[styleName] = newProps.style[styleName];
            }
        } else if (key === 'class') {
            elm.className = newProps.class;
        } else {
            elm.setAttribute(key, newProps[key]);
        }
    }
}
function createElm(vnode) {
    const { tag, data, children, text } = vnode;
    //如果tag是一个字符串的话，说明tag是一个原生的DOM元素 p span div
    if (isString(tag)) {
        //创建真实DOM并且赋值到虚拟DOM的el属性上
        vnode.elm = document.createElement(tag);
        //更新属性 
        updateDOMProperties(vnode.elm, {}, data);
        //遍历子虚拟DOM元素数组
        children.forEach(child => {
            // 把每个子虚拟DOM变成真实DOM并且插入到父节点真实DOM中
            return vnode.elm.appendChild(createElm(child));
        });
    } else {//如果tag不是字符串，则认为它是一个文本节点
        vnode.elm = document.createTextNode(text);
    }
    return vnode.elm;
}
/**
 * 把vnode从虚拟DOM生成真实DOM并且替换掉app
 * @param {*} app 
 * @param {*} vnode 
 */
export function mount(app, vnode) {
    //把虚拟DOM生成真实DOM
    let elm = createElm(vnode);
    //用生成的真实DOM替换掉页面上的app这个挂载容器
    app.replaceWith(elm);
}
function createKeyToOldIdx(children, beginIdx, endIdx) {
    //先声明一个空对象
    let map = {};
    //遍历数组中未处理的区域
    for (let i = beginIdx; i <= endIdx; i++) {
        //如果此虚拟DOM有key的话
        let { key } = children[i];
        //并且key不为空
        if (isDef(key)) {
            //放key和此虚拟DOM对应的索引放在map对象中
            map[key] = i;
        }
    }
    return map;
}
//在老的数组中找可复用的节点
function findIdxInOld(vnode,children,startIdx,endIdx){
    //遍历区间内的节点
    for(let i=startIdx;i<=endIdx;i++){
        //获取每一个节点
        let child = children[i];
        //如果此节点和vnode是同一个节点的话 tag&&key
        if(isDef(child) && sameVnode(child,vnode)){
            //直接返回索引
            return i;
        }
    }
}
/**
 * 对比父元素的子元素
 * @param {*} parentElm 父真实DOM元素
 * @param {*} oldChildren 老的子虚拟DOM数组
 * @param {*} newChildren 新的子虚拟DOM数组
 */
function updateChildren(parentElm, oldChildren, newChildren) {
    let oldStartIdx = 0;//老的头节点索引
    let oldStartVnode = oldChildren[oldStartIdx];//老的头节点
    let oldEndIdx = oldChildren.length - 1;//老的尾节点的索引
    let oldEndVnode = oldChildren[oldEndIdx];//老的尾节点

    let newStartIdx = 0;//新的头节点索引
    let newStartVnode = newChildren[newStartIdx];//新的头节点
    let newEndIdx = newChildren.length - 1;//新的尾节点的索引
    let newEndVnode = newChildren[newEndIdx];//新的尾节点

    let refElm, oldKeyToIdx, idxInOld, vnodeToMove;
    //比较的循环条件，就是只要有任何一方遍历完成，跳出此循环
    //如果新老节点都没有遍历完成的话则会继续循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        //比较老头和新头是否是同一个节点，对应的老的DOM节点不需要移动，保持原位置即可
        if (isUndef(oldStartVnode)) {
            oldStartVnode = oldChildren[++oldStartIdx];
        } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldChildren[--oldEndIdx];
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            //如果是同一个节点，则复用此老节点
            patch(oldStartVnode, newStartVnode);
            //移动指针和节点
            //让老头向后移动一位
            oldStartVnode = oldChildren[++oldStartIdx];
            //让新头向后移动一位
            newStartVnode = newChildren[++newStartIdx];
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            //如果是同一个节点，则复用此老节点
            patch(oldEndVnode, newEndVnode);
            //移动指针和节点
            //让老尾向前移动一位
            oldEndVnode = oldChildren[--oldEndIdx];
            //让新尾向前移动一位
            newEndVnode = newChildren[--newEndIdx];
            //比较老头和新尾    
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            //如果是同一个节点，则复用此老节点
            patch(oldStartVnode, newEndVnode);
            //移动节点
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            //移动指针
            //让老头向后移动一位
            oldStartVnode = oldChildren[++oldStartIdx];
            //让新尾向前移动一位
            newEndVnode = newChildren[--newEndIdx];
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            //如果是同一个节点，则复用此老节点
            patch(oldEndVnode, newStartVnode);
            //把老的尾部真实DOM元素移动到老的开始节点的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            //移动指针
            //让老尾向前移动一位
            oldEndVnode = oldChildren[--oldEndIdx];
            //让新头向后移动一位
            newStartVnode = newChildren[++newStartIdx];
        } else {
            //如果没有命中任何的优化逻辑头头 尾尾 头尾 尾头
            if (isUndef(oldKeyToIdx)) {
                oldKeyToIdx = createKeyToOldIdx(oldChildren, oldStartIdx, oldEndIdx);
            }
            //按新节点的key到map中找可以复用的老节点的索引
            idxInOld = isDef(newStartVnode.key)?oldKeyToIdx[newStartVnode.key]:
            findIdxInOld(newStartVnode,oldChildren,oldStartIdx,oldEndIdx);
            //如果找不到可以复用的老节点
            if (isUndef(idxInOld)) {
                //创建一个新元素，并且插入到未处理的区域的起始节点的的前面或者说左边
                parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm);
            } else {
                //如果找到了可以复用的老节点
                vnodeToMove = oldChildren[idxInOld];
                //主要再判断一下类型一样不一样
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    //更新可以复用的节点
                    patch(vnodeToMove, newStartVnode);
                    //因为此节点被 复用了，并且移动走了，所以此处在数组把此索引对应的值设置为undefined,以后遍历到的时候跳过去
                    oldChildren[idxInOld] = undefined;
                    //把它移动到未处理的区域的左侧节点的左边
                    parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm);
                }else{//如果key一样，但是类型不同，则不能复用
                    parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm);
                }
            }
            //不管是复用老节点还是插入了新节点，都要向后移动新节点的开始指针
            newStartVnode = newChildren[++newStartIdx]
        }
    }
    //如果老头大于老尾，说明老的遍历完了，要插入新的
    if (oldStartIdx > oldEndIdx) {
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            //判断未处理的新儿子的结束索引，的下一位是否一个真实DOM元素，如果果的话插到它的前面
            refElm = isUndef(newChildren[newEndIdx + 1]) ? null : newChildren[newEndIdx + 1].elm;
            //如果refElm为null,insertBefore  null就等同于appendChild
            parentElm.insertBefore(createElm(newChildren[i]), refElm);
            /*   if(refElm){
                  //如果后面有真实DOM，就插入到后面真实DOM的前一位
                  parentElm.insertBefore(createElm(newChildren[i]),refElm);
              }else{
                  //如果没有则添加到父节点的子节点尾部
                  parentElm.appendChild(createElm(newChildren[i]));
              } */
        }
        //如果新头大于新尾，则意味着新的遍历完,删除老节点
    } else if (newStartIdx > newEndIdx) {
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            oldChildren[i]&&parentElm.removeChild(oldChildren[i].elm);
        }
    }
}
export function patchVnode(oldVnode, newVnode) {
    //如果老的虚拟DOM对象和新的虚拟DOM对象完全相同，是同一个内存地址，则不再比较直接返回
    if (oldVnode === newVnode) {
        return;
    }
    //因为到了这里准备要复用老的DOM节点了
    //先把oldVnode上的真实DOM节点取出来，同步到newVnode的elm属性上
    const elm = (newVnode.elm = oldVnode.elm);
    //如果新的虚拟DOM节点的文本为空，说明新节点是一个普通元素
    if (isUndef(newVnode.text)) {
        //更新老的真实DOM属性
        updateDOMProperties(elm, oldVnode.data, newVnode.data);
        //获取老的子节点数组
        const oldChildren = oldVnode.children || [];
        //获取新的子节点的数组
        const newChildren = newVnode.children || [];
        if (oldChildren.length > 0 && newChildren.length > 0) {
            //详细的比较子节点
            updateChildren(elm, oldChildren, newChildren);
        } else if (oldChildren.length > 0) {
            //老儿子存在，但是新儿子不存在,则删除所有的真实DOM子元素
            elm.innerHTML = '';
        } else if (newChildren.length > 0) {
            //老儿子不存在，新儿子存在
            newChildren.forEach(newChild => {
                elm.appendChild(createElm(newChild));
            });
        }
    } else {
        //新旧都是文本节点，而且文本内容不同
        if (oldVnode.text !== newVnode.text) {
            //则把老的文本内容更新为新的文本内容
            oldVnode.elm.textContent = newVnode.text;
        }
        return;
    }
}
/**
 * 比旧新的和老的虚拟DOM节点
 * @param {*} oldVnode 老的虚拟DOM
 * @param {*} newVnode 新的虚拟DOM
 */
export function patch(oldVnode, newVnode) {
    //如果老的虚拟DOM节点和新的虚拟DOM节点是同一个节点
    if (sameVnode(oldVnode, newVnode)) {
        return patchVnode(oldVnode, newVnode);
    } else {
        //如果新旧节点不一样，则删除老节点，添加新节点
        return oldVnode.elm.replaceWith(createElm(newVnode));
    }
}